--[[ 
    通用限流脚本，主控制类

    Author: mahaixing@gmail.com
    Version: 0.0.1
    License: MIT
]]
-- 导入数学模块
local math = require "math"
-- 导入工具类
local utils = require "utils"

local assert = assert
local setmetatable = setmetatable

-- 默认 who 函数
local function default_who(limit)
    return true, {rule = "default_rule"}
end

-- 默认 message 函数
local function default_message(limit, data)
    ngx.header.content_type = "text/html; charset=utf-8"
    ngx.say("#limited#")
    ngx.exit(ngx.exit(ngx.HTTP_OK))
end

local _M = {
    _VERSION = "0.0.1"
}

--[[
    创建新对象
]]
function _M:new(conf)
    -- 如果没有传入配置 table，则引入默认的 limit_conf.lua
    -- 该文件需要在 package.path 环境变量指向的目录中
    if conf == nil then
        conf = require "limit_conf"
    end

    -- 新创建的对象
    local object = {
        -- 配置文件
        conf = conf,

        -- http请求参数，包含 GET 和 POST 
        args = utils.get_url_args(),

        -- redis 链接，需要调用 get_redis 方法初始化获得，需要在
        -- message方法中关闭
        redis = nil,

        -- who 指向的方法，用于确定限流目标
        -- who 指向的可以是 function 也可以是包含 function 的 table
        -- message 方法，用于输出
        funcs = {
            who = default_who,
            message = default_message
        }
    }
    self.__index = self
    return setmetatable(object, self)
end

--[[
    执行跳转，根据请求头 ACCETP 部分，如果是 json 则序列化data
    否则跳转到 page_path
]]
function _M:redirect(page_path, data)
    utils.send_redirect(page_path, data)
end

--[[
    初始化redis链接，redis链接在 config.redis 中配置
    返回 nil 说明配置文件中没有配置 redis 参数，或者初
    始化 redis 错误
]]
function _M:get_redis()
    -- 如果没有配置 redis 参数，则返回nil
    if self.conf["redis"] == nil then return nil end
    -- 如果初始化过了，则返回现有实例（需要考虑被关闭情况）
    if self.redis ~= nil then return self.redis end
    
    local host    = assert(self.conf["redis"]["host"])
    local port    = assert(self.conf["redis"]["port"])
    local auth    = self.conf["redis"]["auth"]
    local timeout = 500
    if self.conf["redis"]["timeout"] ~= nil then
        local timeout = self.conf["redis"]["timeout"]
    end

    -- 导入redis模块
    self.redis = require "resty.redis":new()
    self.redis:set_timeout(timeout)
    local ok, err = self.redis:connect(host, port)
    if auth ~= nil then
        self.redis:auth(auth)
    end
    
    if not ok then
        utils.log("connect to redis failed: " .. err, ngx.ERR)
        error("connect to redis failed: " .. err)
    end
    
    return self.redis
end

--[[
    执行 who 可以指向：
    1. function
    2、包含一系列 function 的 table
    3. nil，如果这种情况会使用默认的键名 default_rule
    
    function 方法返回 2 个值：
    1. boolean 用于确定当前是否是该方法关心的限流对象
    2. table 
        result: 必填，返回结果 true false
        rule: 非必填，如不提供则使用默认规则
        data: 非必填，如不提供则不处理，如提供则可在 message 函数中处理
    
    当某一个function 返回 true 后，
]]
function _M:who(funcs) 
    if funcs ~= nil then
        self.funcs.who = funcs 
    end

    return self
end

-- 执行 who 函数
function _M:process_who()
    local result, rule

    if (type(self.funcs.who) == "table") then
        for _, func in pairs(self.funcs.who) do
            result, extra = func(self)
            if result then break end
        end
    elseif (type(self.funcs.who) == "function") then
        result, extra = self.funcs.who(self)
    end

    return result, extra
end

-- 执行 message 函数
function _M:process_message(data)
    self.funcs.message(self, data)
end

-- 获取 who 函数返回的限流规则
function _M:process_rule(rule)
    self.rule = self.conf[rule]
    if self.rule == nil then
        error("returned rule does not exist!")
        utils.log("returned rule does not exist!", ngx.ERR)
    end
end

-- 执行限流动作
function _M:execute()
    -- 执行 who 函数，返回：
    -- 1. 是否是限流目标 boolean 2. 适用的限流规则 3. 可能的数据如果message函数需要处理的话
    local who_result, extra = self:process_who()
    -- 如果所有的 who 函数都返回 false 说明这个请求不是要限流的目标，直接放行
    if (not who_result) then
        return
    end
    
    if (extra.rule ~= nil) then 
        -- 拿到限流规则
        self:process_rule(extra.rule)
        utils.log("use rule: " .. extra.rule, ngx.INFO)
    end

    if (extra.message ~= nil) then
        self.message = extra.message
    end
    
    -- see_next 是用来确定限流规则是否需要参考限流链的下一个规则。
    local result = true, see_next
    -- 循环执行限流规则
    for limiter_name, limiter_conf in pairs(self.rule) do
        -- 包含限流规则 lua 文件
        local limiter = require("limiters." ..  limiter_name)
        -- 执行限流
        local instance = limiter:new(limiter_conf)
        instance:process_config()
        result, see_next = instance:execute()
        -- 如果限流返回 true 则说明此链接已被此规则限流
        -- 如果 see_next 返回 true 则说明需要参考限流链的吓一条限流规则
        -- 否则不执行限流链的后面一条规则，跳出循环
        if (see_next == nil) then see_next = false end
        if (result == true) or (see_next == false) then
            break
        end
    end

    -- 返回到客户浏览器
    if result then
        self:process_message(extra.data)
    end
end

return _M